<!-- 
  拼图验证码
  用法：
    简易：
      <puzzle-code
        v-model="visible"
        @success="success"
        @close="close"
        @fail="fail"
        @masked="masked"
      />
    完整：
      <puzzle-code
        v-model="visible"
        :img-width="imgWidth"
        :img-height="imgHeight"
        :slider-size="sliderSize"
        :success-text="successText"
        :fail-text="failText"
        :slider-text="sliderText"
        :border-radius="borderRadius"
        :build-puzzle-code-url="buildPuzzleCodeUrl"
        :build-puzzle-code-method="buildPuzzleCodeMethod"
        :verify-code-url="verifyCodeUrl"
        :verify-code-method="verifyCodeMethod"
        :reset-img="resetImg"
        @success="success"
        @close="close"
        @fail="fail"
        @masked="masked"
      />
  说明：
    属性：
      v-model(value)：绑定一个boolean值，用来显示或隐藏窗口
      imgWidth: 图片宽度，默认280，单位px
      imgHeight: 图片高度，默认150，单位px
      sliderSize: 滑块大小，默认35，单位px
      successText: 验证成功提示
      failText: 验证失败提示
      sliderText: 滑块提示
      borderRadius: 圆弧，默认10，单位px
      buildPuzzleCodeUrl：验证码图片接口，默认"/login/getPuzzleVerificationCode"
      buildPuzzleCodeMethod：验证码图片接口请求方式，默认get
      verifyCodeUrl: 验证码验证接口，默认"/login/puzzleVerifyCode/verify"
      verifyCodeMethod: 验证码验证接口请求方式，默认get
      resetImg: 重置按钮图片地址
    事件：
      success：验证成功回调
      fail：验证失败回调
      masked：点击遮罩层回调
      close：点击关闭回调
-->
<template>
  <div
    :class="['puzzle-code', { 'puzzle-code-show': visible }]"
    @click="onMasked"
  >
    <div
      class="puzzle-code-box"
      :style="{ 'border-radius': borderRadius + 'px' }"
      @mousedown.stop
      @touchstart.stop
    >
      <div class="box-header">
        <span class="el-icon-close close-btn" @click="onClose" />
      </div>
      <div class="box-body" :style="`height: ${imgHeight}px`">
        <img :src="artworkImg" :width="imgWidth" :height="imgHeight" />
        <img
          :src="vacancyImg"
          class="vacancy-img"
          :height="imgHeight"
          :style="`transform:translateX(${styleWidth - sliderSizeConvert}px)`"
        />
        <div :class="['loading-box', { 'loading-hide': !loading }]">
          <div class="loading-body">
            <span />
            <span />
            <span />
            <span />
            <span />
          </div>
        </div>
        <div
          :class="[
            'info-box',
            { 'show-info-box': infoBoxShow },
            { 'valid-fail-info': isValidFail },
          ]"
        >
          {{ infoText }}
        </div>
        <div
          :class="['flash-btn', { visible: validSuccess }]"
          :style="`transform: translateX(${
            validSuccess
              ? `${imgWidth + imgHeight * 0.578}px`
              : `-${imgHeight * 0.578}px`
          }) skew(-30deg, 0);`"
        />
        <img class="reset-img" :src="resetImg" @click="buildPuzzleCode" />
      </div>
      <div class="auth-control">
        <div class="range-box" :style="`height:${sliderSizeConvert}px`">
          <div class="range-text">{{ sliderText }}</div>
          <div
            ref="range-slider"
            class="range-slider"
            :style="`width:${styleWidth}px`"
          >
            <div
              :class="['range-btn', { 'mouse-down-btn': mouseDown }]"
              :style="`width:${sliderSizeConvert}px`"
              @mousedown="onRangeMouseDown($event)"
              @touchstart="onRangeMouseDown($event)"
            >
              <div />
              <div />
              <div />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import request from "@/utils/request";

export default {
  name: "puzzle-code",
  props: {
    value: {
      type: Boolean,
      require: true,
      default: false,
    },
    imgWidth: {
      type: Number,
      require: false,
      default: 280,
    },
    imgHeight: {
      type: Number,
      require: false,
      default: 150,
    },
    sliderSize: {
      type: Number,
      require: false,
      default: 35,
    },
    successText: {
      type: String,
      require: false,
      default: "验证成功！",
    },
    failText: {
      type: String,
      require: false,
      default: "验证失败，请重试！",
    },
    sliderText: {
      type: String,
      require: false,
      default: "拖动滑块完成拼图",
    },
    borderRadius: {
      type: Number,
      require: false,
      default: 10,
    },
    buildPuzzleCodeUrl: {
      type: String,
      require: false,
      default: "/login/getPuzzleVerificationCode",
    },
    buildPuzzleCodeMethod: {
      type: String,
      require: false,
      default: "get",
    },
    verifyCodeUrl: {
      type: String,
      require: false,
      default: "/login/puzzleVerifyCode/verify",
    },
    verifyCodeMethod: {
      type: String,
      require: false,
      default: "get",
    },
    resetImg: {
      type: String,
      require: false,
      default: require("./reset.png"),
    },
  },
  data() {
    return {
      visible: this.value,
      artworkImg: undefined,
      vacancyImg: undefined,
      mouseDown: false, // 鼠标是否在按钮上按下
      startWidth: 50, // 鼠标点下去时父级的width
      startX: 0, // 鼠标按下时的X
      currentX: 0, // 鼠标当前的偏移X
      loading: true,
      isMoveSlide: false, // 是否可以拉动滑动条
      infoBoxShow: false,
      infoText: "",
      isValidFail: false,
      timeoutId: null,
      validSuccess: false,
    };
  },
  computed: {
    // styleWidth是底部用户操作的滑块的父级，就是轨道在鼠标的作用下应该具有的宽度
    styleWidth() {
      const w = this.startWidth + this.currentX - this.startX;
      return w < this.sliderSizeConvert
        ? this.sliderSizeConvert
        : w > this.imgWidth
        ? this.imgWidth
        : w;
    },
    // 处理一下sliderSize，弄成整数，以免计算有偏差
    sliderSizeConvert() {
      return Math.max(
        Math.min(Math.round(this.sliderSize), Math.round(this.imgWidth * 0.5)),
        10
      );
    },
  },
  watch: {
    value(v) {
      if (this.visible != v) {
        this.visible = v;
      }
      if (v) {
        this.initPuzzleCode();
      }
    },
  },
  mounted() {
    document.addEventListener("mousemove", this.onRangeMouseMove, false);
    document.addEventListener("mouseup", this.onRangeMouseUp, false);
    document.addEventListener("touchmove", this.onRangeMouseMove, {
      passive: false,
    });
    document.addEventListener("touchend", this.onRangeMouseUp, false);

    if (this.visible) {
      this.initPuzzleCode();
    }
  },
  beforeDestroy() {
    clearTimeout(this.timeoutId);
    document.removeEventListener("mousemove", this.onRangeMouseMove, false);
    document.removeEventListener("mouseup", this.onRangeMouseUp, false);

    document.removeEventListener("touchmove", this.onRangeMouseMove, {
      passive: false,
    });
    document.removeEventListener("touchend", this.onRangeMouseUp, false);
  },
  methods: {
    onMasked() {
      this.$emit("masked");
    },
    onClose() {
      clearTimeout(this.timeoutId);
      this.visible = false;
      this.$emit("close");
    },
    // 鼠标按下准备拖动
    onRangeMouseDown(e) {
      if (this.isMoveSlide) {
        this.mouseDown = true;
        this.startWidth = this.$refs["range-slider"].clientWidth;
        this.currentX = e.clientX || e.changedTouches[0].clientX;
        this.startX = e.clientX || e.changedTouches[0].clientX;
      }
    },
    // 鼠标移动
    onRangeMouseMove(e) {
      if (this.mouseDown) {
        e.preventDefault();
        this.currentX = e.clientX || e.changedTouches[0].clientX;
      }
    },
    // 鼠标抬起
    onRangeMouseUp() {
      if (this.mouseDown) {
        this.mouseDown = false;
        this.verify();
      }
    },
    buildPuzzleCode() {
      this.loading = true;
      this.isMoveSlide = false;
      request({
        url: this.buildPuzzleCodeUrl,
        method: this.buildPuzzleCodeMethod,
      })
        .then((res) => {
          const { success, message, parameters } = res;
          if (success) {
            const { artworkImg, vacancyImg } = parameters;
            this.artworkImg = artworkImg;
            this.vacancyImg = vacancyImg;
            this.loading = false;
            this.isMoveSlide = true;
            this.startTime = new Date().getTime();
          } else {
            this.loading = false;
            this.msgError(message);
          }
        })
        .catch((err) => {
          this.loading = false;
        });
    },
    verify() {
      this.isMoveSlide = false;
      const x = this.currentX - this.startX;
      this.loading = true;
      request({
        url: this.verifyCodeUrl,
        method: this.verifyCodeMethod,
        params: {
          xOffset: x,
          imageWidth: this.imgWidth,
        },
      })
        .then((res) => {
          const { success, message, parameters } = res;
          this.loading = false;
          if (success) {
            const { activationCode, activationTime } = parameters;
            this.verifySuccessHandle(activationCode, activationTime);
          } else {
            this.verifyFailHandle(message);
          }
        })
        .catch((err) => {
          this.loading = false;
          this.verifyFailHandle(err);
        });
    },
    verifySuccessHandle(activationCode, activationTime) {
      this.infoText = this.successText;
      this.isValidFail = false;
      this.infoBoxShow = true;
      this.isMoveSlide = false;
      this.validSuccess = true;
      clearTimeout(this.timeoutId);
      this.timeoutId = setTimeout(() => {
        this.visible = false;
        this.$emit("success", {
          activationCode: activationCode,
          activationTime: activationTime,
        });
      }, 800);
    },
    verifyFailHandle(err) {
      this.infoText = this.failText;
      this.isValidFail = true;
      this.infoBoxShow = true;
      this.isMoveSlide = false;
      this.$emit("fail", err);
      clearTimeout(this.timeoutId);
      this.timeoutId = setTimeout(() => {
        this.initPuzzleCode();
      }, 800);
    },
    initPuzzleCode() {
      this.isValidFail = false;
      this.infoBoxShow = false;
      this.isMoveSlide = true;
      this.validSuccess = false;
      this.startWidth = this.sliderSizeConvert;
      this.startX = 0; // 鼠标按下时的X
      this.currentX = 0; // 鼠标当前的偏移X
      this.buildPuzzleCode();
    },
  },
};
</script>

<style lang="scss" scoped>
.puzzle-code {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 999;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms;
  &.puzzle-code-show {
    opacity: 1;
    pointer-events: auto;
  }
}
.puzzle-code-box {
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 0 20px 20px 20px;
  background: #fff;
  user-select: none;
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  .box-header {
    display: flex;
    flex-flow: row nowrap;
    justify-content: flex-end;
    margin: 10px 0 5px 5px;
    .close-btn {
      font-size: 21px;
      cursor: pointer;
    }
  }
  .box-body {
    position: relative;
    overflow: hidden;
    border-radius: 3px;
    .loading-box {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.8);
      z-index: 20;
      opacity: 1;
      transition: opacity 200ms;
      display: flex;
      align-items: center;
      justify-content: center;
      &.loading-hide {
        opacity: 0;
        pointer-events: none;
        .loading-body {
          span {
            animation-play-state: paused;
          }
        }
      }
      .loading-body {
        flex: none;
        height: 5px;
        line-height: 0;
        @keyframes load {
          0% {
            opacity: 1;
            transform: scale(1.3);
          }
          100% {
            opacity: 0.2;
            transform: scale(0.3);
          }
        }
        span {
          display: inline-block;
          width: 5px;
          height: 100%;
          margin-left: 2px;
          border-radius: 50%;
          background-color: #888;
          animation: load 1.04s ease infinite;
          &:nth-child(1) {
            margin-left: 0;
          }
          &:nth-child(2) {
            animation-delay: 0.13s;
          }
          &:nth-child(3) {
            animation-delay: 0.26s;
          }
          &:nth-child(4) {
            animation-delay: 0.39s;
          }
          &:nth-child(5) {
            animation-delay: 0.52s;
          }
        }
      }
    }
    .info-box {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 24px;
      line-height: 24px;
      text-align: center;
      overflow: hidden;
      font-size: 13px;
      background-color: #83ce3f;
      opacity: 0;
      transform: translateY(24px);
      transition: all 200ms;
      color: #fff;
      z-index: 10;
      &.show-info-box {
        opacity: 0.95;
        transform: translateY(0);
      }
      &.valid-fail-info {
        background-color: #ce594b;
      }
    }
    .vacancy-img {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
    }
    .flash-btn {
      position: absolute;
      top: 0;
      left: 0;
      width: 30px;
      height: 100%;
      background-color: rgba(255, 255, 255, 0.1);
      z-index: 3;
      &.visible {
        transition: transform 600ms;
      }
    }
    .reset-img {
      position: absolute;
      top: 2px;
      right: 2px;
      width: 35px;
      height: 35px;
      z-index: 12;
      cursor: pointer;
      transition: transform 200ms;
      transform: rotate(0deg);
      &:hover {
        transform: rotate(-90deg);
      }
    }
  }
  .auth-control {
    .range-box {
      position: relative;
      width: 100%;
      background-color: #eef1f8;
      margin-top: 20px;
      border-radius: 3px;
      box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;
      .range-text {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: 14px;
        color: #b7bcd1;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        text-align: center;
        width: 100%;
      }
      .range-slider {
        position: absolute;
        height: 100%;
        width: 50px;
        background-color: rgba(106, 160, 255, 0.8);
        border-radius: 3px;
        .range-btn {
          position: absolute;
          display: flex;
          align-items: center;
          justify-content: center;
          right: 0;
          width: 50px;
          height: 100%;
          background-color: #fff;
          border-radius: 3px;
          box-shadow: 0 0 4px #ccc;
          cursor: pointer;
          & > div {
            width: 0;
            height: 40%;

            transition: all 200ms;
            &:nth-child(2) {
              margin: 0 4px;
            }
            border: solid 1px #6aa0ff;
          }
          &:hover,
          &.mouse-down-btn {
            & > div:first-child {
              border: solid 4px transparent;
              height: 0;
              border-right-color: #6aa0ff;
            }
            & > div:nth-child(2) {
              border-width: 3px;
              height: 0;
              border-radius: 3px;
              margin: 0 6px;
              border-right-color: #6aa0ff;
            }
            & > div:nth-child(3) {
              border: solid 4px transparent;
              height: 0;
              border-left-color: #6aa0ff;
            }
          }
        }
      }
    }
  }
}
</style>
